package net.zoneland.zrdp.framework.web.service;

import java.io.Serializable;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;

import javax.annotation.Resource;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import com.richercloud.lift.util.function.enhancer.FunctionEnhancer;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.JsonProcessingException;

import eu.bitwalker.useragentutils.UserAgent;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import net.zoneland.zrdp.common.constant.CacheConstants;
import net.zoneland.zrdp.common.constant.Constants;
import net.zoneland.zrdp.common.core.domain.model.LoginUser;
import net.zoneland.zrdp.common.core.redis.RedisService;
import net.zoneland.zrdp.common.utils.JacksonUtils;
import net.zoneland.zrdp.common.utils.ServletUtils;
import net.zoneland.zrdp.common.utils.ip.AddressUtils;
import net.zoneland.zrdp.common.utils.ip.IpUtils;
import net.zoneland.zrdp.common.utils.uuid.IdUtils;

/**
 * token验证处理
 *
 * @author zonevue
 */
@Component
@ConfigurationProperties(prefix = "token")
public class TokenServiceImpl implements TokenService, InitializingBean {
    public static final String USER_CACHE = "_user";

    //空用户,用于一次request级缓存,防止因AOP出现方法调死循环
    public static final LoginUser EMPTY_USER = new LoginUser();

    private static final Logger LOGGER = LoggerFactory.getLogger(TokenServiceImpl.class);
    // 令牌自定义标识
    private String header;

    /**
     * Cookie中定义的令牌标识
     */
    private String cookieHeader;

    // 令牌秘钥
    private String secret;

    // 令牌有效期（默认30分钟）
    private int expireTime;

    /**
     * 启动jvm级user缓存: -Dtoken.cacheOnJvm=true.
     */
    private boolean cacheOnJvm = true;

    /**
     * 缓存user的过期秒数(默认10分钟),
     */
    private int cacheOnJvmExpiredMs = 600000;

    protected static final long MILLIS_SECOND = 1000;

    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

    private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;

    @Resource
    private RedisService redisService;

    @Resource
    private ObjectProvider<CaffeineCacheManager> caffeineCacheManagerSpi;

    private Cache userJvmCache;

    public LoginUser getCachedUser(final HttpServletRequest request, final String token) {
        final LoginUser cached1 = (LoginUser) request.getAttribute(USER_CACHE);
        if (cached1 != null){
            return cached1;
        }
        if (cacheOnJvm && token != null) {
            final LoginUserCacheEntity cachedUser = getCachedUserInJvm(token);
            if (cachedUser != null) {
                return cachedUser.getUser();
            }
        }
        return null;
    }

    /**
     * 获取用户身份信息.
     *  设计了三级缓存, request(一级), jvm(二级,禁用设置cacheOnJvm=false),  redis(三级)
     *
     * @return 用户信息
     */
    public LoginUser getLoginUser(final HttpServletRequest request) {
        return getLoginUser(request, true);
    }

    public LoginUser getLoginUser(final HttpServletRequest request, final boolean headerToken) {
        // 获取请求携带的令牌
        final String token = getToken(request, headerToken);
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        final LoginUser user = getCachedUser(request, token);
        if (user == EMPTY_USER) {
            return null;
        }
        if (user != null) {
            user.setBrowserToken(token);
            return user;
        }

        try {
            //优先设计请求级缓存,是为防止AOP时内部再调用此方法导致死循环
            request.setAttribute(USER_CACHE, EMPTY_USER);
            final LoginUser tmpUser = getUserFromToken(token);
            if (tmpUser != null ) {
                //request级缓存(一级)
                request.setAttribute(USER_CACHE, tmpUser);
                if (cacheOnJvm) {
                    //jvm 级存(二级)
                    cacheUserInJvm(tmpUser, token);
                }
                tmpUser.setBrowserToken(token);
                return tmpUser;
            }
        } catch (final Exception e) {
            LOGGER.error("获取用户失败{}", token, e);
        }
        return null;
    }

    /**
     * 从令牌中获取用户信息
     *
     * @param token 令牌
     * @return 用户信息
     */
    private LoginUser getUserFromToken(final String token) throws JsonProcessingException {
        final String userKey = toStoreTokenKey(token);
        return JacksonUtils.toObject(redisService.getCacheString(userKey), LoginUser.class);
    }

    private String toStoreTokenKey(final String token) {
        final Claims claims = parseToken(token);
        final String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
        return getTokenKey(uuid);
    }

    /**
     * 设置用户身份信息
     */
    public void setLoginUser(final LoginUser loginUser) {
        if (Objects.nonNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) {
            refreshToken(loginUser);
        }
    }

    /**
     * 删除用户身份信息
     */
    public void delLoginUser(final LoginUser loginUser) {
        final String token = loginUser.getToken();
        if (StringUtils.isEmpty(token)) {
            return;
        }
        final String userKey = getTokenKey(token);
        redisService.deleteObject(userKey);
        if (userJvmCache == null) {
            return;
        }
        final String browserToken = loginUser.getBrowserToken();
        if (browserToken == null) {
            removeCacheByStoreToken(token, new AtomicInteger(0));
        } else {
            final boolean removed = userJvmCache.evictIfPresent(browserToken);
            LOGGER.info("evictUserCacheOnJvm: {} <- {}", removed, browserToken);
        }
    }

    /**
     * 创建令牌
     *
     * @param loginUser 用户信息
     * @return 令牌
     */
    public String createToken(final LoginUser loginUser) {
        final String token = IdUtils.fastUUID();
        loginUser.setToken(token);
        setUserAgent(loginUser);
        refreshToken(loginUser);

        final Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY, token);
        return createToken(claims);
    }

    /**
     * 验证令牌有效期，相差不足20分钟，自动刷新缓存
     *
     * @param loginUser 登录用户
     */
    public void verifyToken(final LoginUser loginUser) {
        final long userExpireTime = loginUser.getExpireTime();
        final long currentTime = System.currentTimeMillis();
        if (userExpireTime - currentTime <= MILLIS_MINUTE_TEN) {
            refreshToken(loginUser);
        }
    }

    /**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshToken(final LoginUser loginUser) {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        final String userKey = getTokenKey(loginUser.getToken());
        try {
            redisService.setCacheString(userKey, JacksonUtils.toJsonString(loginUser), expireTime, TimeUnit.MINUTES);
        } catch (final JsonProcessingException e) {
            LOGGER.error("数据转jsonstr 出错{}", loginUser, e);
        }
    }

    /**
     * 设置用户代理信息
     *
     * @param loginUser 登录信息
     */
    public void setUserAgent(final LoginUser loginUser) {
        final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
        final String ip = IpUtils.getIpAddr();
        loginUser.setIpaddr(ip);
        loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
        loginUser.setBrowser(userAgent.getBrowser().getName());
        loginUser.setOs(userAgent.getOperatingSystem().getName());
    }

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String createToken(final Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims parseToken(final String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(final String token) {
        final Claims claims = parseToken(token);
        return claims.getSubject();
    }

    /**
     * 获取请求token
     *
     * @param request - http请求
     * @param headerToken - 是否从header中获取Token
     * @return token
     */
    private String getToken(final HttpServletRequest request, final boolean headerToken) {
        final String token = headerToken ? StringUtils.removeStart(request.getHeader(header), Constants.TOKEN_PREFIX) : null;
        if (StringUtils.isEmpty(token) && cookieHeader != null) {
            final Cookie[] cookies = request.getCookies();
            return cookies == null ? null : Arrays.stream(cookies)
                .filter(FunctionEnhancer.with(Cookie::getName).thenTest(cookieHeader::equals))
                .findFirst() // 必须保证各个项目的 cookieHeader 是不同的值。
                .map(Cookie::getValue)
                .orElse(null);
        }
        return token;

    }

    private String getTokenKey(final String uuid) {
        return CacheConstants.LOGIN_TOKEN_KEY + uuid;
    }

    public final String getHeader() {
        return header;
    }

    public final void setHeader(final String header) {
        this.header = header;
    }

    public final String getSecret() {
        return secret;
    }

    public final void setSecret(final String secret) {
        this.secret = secret;
    }

    public final int getExpireTime() {
        return expireTime;
    }

    public final void setExpireTime(final int expireTime) {
        this.expireTime = expireTime;
    }

    /**
     * 把用户信息保存到seesion中
     * @param user - 用户信息
     * @param token  - cacheKey
     */
    private void cacheUserInJvm(final LoginUser user, final String token) {
        Optional.ofNullable(getUserCache())
            .ifPresent(userCache -> userCache.put(token, new LoginUserCacheEntity(user)));

    }

    /**
     * 从jvm缓存中获取用户信息
     * @param token session
     * @return 用户信息
     */
    private LoginUserCacheEntity getCachedUserInJvm(final String token) {
        if (token == null) {
            return null;
        }
        final Cache userCache = getUserCache();
        if (userCache == null) {
            return null;
        }
        final ValueWrapper entity = userCache.get(token);
        if (entity == null) {
            return null;
        }
        final LoginUserCacheEntity user = (LoginUserCacheEntity) entity.get();
        if (user != null && cacheOnJvmExpiredMs > 0 && user.isNotExpired(cacheOnJvmExpiredMs)) {
            return user;
        }
        return null;
    }

    private Cache getUserCache() {
        if (this.userJvmCache == null) {
            this.userJvmCache = caffeineCacheManagerSpi.getObject().getCache("userCache");
        }
        return this.userJvmCache;
    }

    public static final class LoginUserCacheEntity implements Serializable {
        private static final long serialVersionUID = 1L;
        private final LoginUser user;
        private final long atTime;
        private final AtomicLong accessCount = new AtomicLong(1);

        LoginUserCacheEntity(final LoginUser user) {
            super();
            this.user = user;
            this.atTime = System.currentTimeMillis();
        }

        public boolean isNotExpired(final long expiredTime) {
            accessCount.incrementAndGet();
            return System.currentTimeMillis() - atTime < expiredTime;
        }

        public LoginUser getUser() {
            return user;
        }

        public long getAtTime() {
            return atTime;
        }

        public long getAccessCount() {
            return accessCount.get();
        }
        public boolean isSameToken(final String token) {
            return token.equals(user.getToken());
        }
    }

    public String getCookieHeader() {
        return cookieHeader;
    }

    public void setCookieHeader(final String cookieHeader) {
        this.cookieHeader = cookieHeader;
    }

    public final int getCacheOnJvmExpiredMs() {
        return cacheOnJvmExpiredMs;
    }

    public final void setCacheOnJvmExpiredMs(final int cacheOnServletSessionExpiredMs) {
        this.cacheOnJvmExpiredMs = cacheOnServletSessionExpiredMs;
    }

    public final boolean isCacheOnJvm() {
        return cacheOnJvm;
    }

    public final boolean setCacheOnJvm(final boolean cacheOnJvm) {
      if (caffeineCacheManagerNotFound) {
            this.cacheOnJvm = false;
        } else {
            this.cacheOnJvm = cacheOnJvm;
        }
        return cacheOnJvm;
    }

    private boolean caffeineCacheManagerNotFound;

    @Override
    public void afterPropertiesSet() throws Exception {
        caffeineCacheManagerNotFound = caffeineCacheManagerSpi == null || caffeineCacheManagerSpi.getIfAvailable() == null;
        if (caffeineCacheManagerNotFound) {
            this.cacheOnJvm = false;
        }
    }

    public void evictUserCacheOnJvm(final String token) {
        if (userJvmCache != null){
            final AtomicInteger removeCount = new AtomicInteger();
            if (!userJvmCache.evictIfPresent(token)) {
                removeCacheByStoreToken(token, removeCount);
            } else {
                removeCount.incrementAndGet();
            }
            LOGGER.info("evictUserCacheOnJvm: {} <- {}", removeCount, token);
        } else {
            LOGGER.info("evictUserCacheOnJvm do nothing for userJvmCache = null");
        }
    }

    private void removeCacheByStoreToken(final String token, final AtomicInteger removeCount) {
        final Map<String, LoginUserCacheEntity> map = ((Supplier<Map<String,LoginUserCacheEntity>>)(userJvmCache.getNativeCache())).get();
        map.entrySet().stream()
            .filter(entry -> entry.getValue() != null && entry.getValue().isSameToken(token))
            .map(Map.Entry::getKey)
            .forEach( x -> {
                if (userJvmCache.evictIfPresent(x)) {
                    removeCount.incrementAndGet();
                }
            });
    }
}
